Once we have a disciplined evaluation protocol, we still need to decide what performance means. A single number such as accuracy can be useful, but it can also be deeply misleading when the classes are imbalanced or when different types of errors have different costs.
This chapter develops the classification metrics needed to look inside a model's decisions: the confusion matrix, accuracy, precision, recall and the Fβ family. We then move from hard class labels to continuous scores, showing how changing the decision threshold trades off true-positive and false-positive rates and leads naturally to the ROC curve, AUC, model comparison, and operating-threshold selection.
Classification evaluation starts with the confusion matrix and the limitations of accuracy, then moves to precision, recall and Fβ before treating the classifier output as a continuous score and studying threshold trade-offs with ROC and AUC.
Every evaluation in this chapter is built on the standard classification accuracy, which is read off the confusion matrix:
| Predicted Label | |||
|---|---|---|---|
| Positive (+) | Negative (−) | ||
| True Label | + | True Positive (TP) | False Negative (FN) |
| − | False Positive (FP) | True Negative (TN) | |
All the evaluation so far has assumed that accuracy is a reasonable summary of performance. When one class is much rarer than the other, that assumption breaks down.
Dataset: 9,990 legitimate transactions (= Class 0), 10 fraud (= Class 1). Total n = 10,000.
A trivial model that predicts "Legitimate" for every single transaction achieves 9,990 / 10,000 = 99.9% accuracy — and 0 frauds caught. High accuracy, completely useless.
| Predicted | Total | |||
|---|---|---|---|---|
| 0 (Legit) | 1 (Fraud) | |||
| True | 0 | 9,990 (TN) | 0 (FP) | 9,990 |
| 1 (Fraud) | 10 (FN) | 0 (TP) | 10 | |
| Total | 9,990 | 0 | 10,000 | |
Accuracy is always reported, but never trusted alone on imbalanced tasks.
To describe performance on an imbalanced problem we need to name the four cells of the confusion matrix separately. A medical screening test is a convenient way to remember them.
From these four counts we can define metrics that, unlike accuracy, do not become meaningless when one class dominates the dataset:
Arithmetic mean = (1.0 + 0.5)/2 = 0.75, which looks acceptable. But a model that flags only one very obvious positive (so it makes no false positives and reaches 100% precision) while missing half of all real positives is not a 75% model. The arithmetic mean hides the poor recall.
Harmonic mean = 2 × 1.0 × 0.5 / (1.0 + 0.5) = 1.0/1.5 = 0.667. It is always ≤ the arithmetic mean, and it is dragged toward the minimum of the two — exactly what we want so we can't game the metric by doing great on one and terrible on the other.
Numeric check: (Precision=0.01, Recall=0.99). The arithmetic mean is 0.50, which again looks acceptable. The harmonic mean is ≈ 0.02, which is the correct verdict: the model is flagging almost everything as positive and is right only 1% of the time when it does.
The pattern holds for any two positive numbers, not just Precision/Recall. Take a = 1 and b = 9: the arithmetic mean is (1 + 9)/2 = 5, but the harmonic mean is 2·1·9/(1 + 9) = 18/10 = 1.8 — pulled sharply toward the smaller number. This is the same pull that makes F1 punish a model that is excellent on one of Precision/Recall but weak on the other.
Most classifiers can return a continuous score, not just a binary label. For KNN with K=19, if 13 neighbors say + and 6 say −, then:
The default threshold is to classify as + if P ≥ 0.5. There is nothing special about 0.5, however. The threshold should be chosen from the cost of each type of error in the application, not taken from convention.
The ROC (Receiver Operating Characteristic) curve was originally developed in the 1950s for signal detection theory — distinguishing a true radar signal from noise — before being adopted for classifier evaluation. It plots TPR (y-axis) against FPR (x-axis) as the decision threshold sweeps across its full range.
Plot each (FPR, TPR) pair. Connect the dots. The result is the ROC curve. It always passes through (0,0) (threshold=1, predict nobody) and (1,1) (threshold=0, predict everybody). Best classifier = hugs the upper-left corner (0,1).
The ROC curve gives a picture of performance across all thresholds. To compare models numerically we summarise the whole curve by the area beneath it:
AUC compares two models by a single number. When we instead need to compare two specific operating points, we compare their positions on the ROC plane directly.
Given two candidate thresholds (or two different models) plotted as points on the FPR-TPR plane:
Point A dominates point B iff A is strictly to the northwest of B: lower FPR AND higher TPR.
If neither dominates the other — one has higher TPR but also higher FPR — which is "better" depends on the per-error costs that the deployment environment assigns.
Example from lecture: Point (0.1, 0.6) dominates (0.2, 0.5) → lower FPR + higher TPR. But (0.1, 0.6) vs. (0.2, 0.7)? The latter has higher TPR but worse FPR, so deployment cost structure decides.
Two automated methods to pick the single "best" point along the ROC curve.
Distance from the perfect classifier at (FPR = 0, TPR = 1):
We minimize Euc across all candidate ROC points, since it measures how close a point lies to the ideal corner. It is 0 for a perfect classifier and at most √2 ≈ 1.414 for a classifier sitting at (1, 0).
Euc is the straight-line distance between an operating point and the ideal top-left corner (0, 1).
The Youden index instead maximizes the vertical distance of the point above the diagonal. It is widely used in the medical literature.
Range [0, 1]. J = 1 only when perfect (TPR=1, FPR=0). J = 0 when the point sits on the random diagonal.
To summarise, the ROC curve serves two distinct purposes, and it is worth keeping them separate:
Classifier on medical diagnosis: 8 sick / 1000 patients total. Confusion matrix below:
| Predicted | Total | |||
|---|---|---|---|---|
| Sick (+) | Healthy (−) | |||
| True | Sick | TP = 8 | FN = 2 | 10 |
| Healthy | FP = 48 | TN = 942 | 990 | |
| Total | 56 | 944 | 1000 | |
Interpretation: 95% accuracy hides a terrible classifier for this specific task. An F1 of 0.24 reflects the disastrous precision (48 healthy people falsely told they are sick). In medical screening, high Recall (e.g., ≥ 95%) is usually the primary KPI to ensure we don't miss sick patients.
For each task, pick β ∈ {0.3, 1, 4} (low, equal, high) to weight Precision vs. Recall appropriately:
Ten test instances with true class and classifier probability P(+|x):
| Instance | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| P(+|x) | 0.95 | 0.93 | 0.87 | 0.85 | 0.80 | 0.78 | 0.76 | 0.53 | 0.43 | 0.25 |
| True Class | + | + | − | + | − | + | − | − | − | + |
Using a 0.5 cutoff, every score at or above the threshold is predicted positive. But 0.5 is just one arbitrary choice among many. Compute the accuracy of this classifier at threshold = 0.5, then again at 0.87 and 0.43, and see whether 0.5 is really the best cutoff for this data.
At Threshold 0.5: Predict + for rows 1–8, − for 9, 10.
TP=4 (rows 1,2,4,6), FP=4 (rows 3,5,7,8), TN=1 (row 9), FN=1 (row 10).
Accuracy = (4+1)/10 = 50%
At Threshold 0.87: Predict + for rows 1,2,3.
TP=2, FP=1, TN=4, FN=3.
Accuracy = (2+4)/10 = 60%
At Threshold 0.43: Predict + for rows 1–9.
TP=4, FP=5, TN=0, FN=1.
Accuracy = (4+0)/10 = 40%
Conclusion: Threshold 0.87 happens to be better than 0.5 here, but checking three thresholds by hand doesn't tell us which cutoff is truly optimal. An ROC curve plots TPR vs. FPR at *all* thresholds so we can see every trade-off at once. Problem 3 in Section 4 (Numerical Solutions) builds that complete table from this same dataset, then finds the optimal threshold using Euc and Youden's J.
Six candidate threshold points (FPR, TPR): (0.1, 0.6), (0.2, 0.5), (0.4, 0.2), (0.5, 0.5), (0.7, 0.7), (0.2, 0.7).
Six operating points on the FPR–TPR plane, with the random-classifier diagonal for reference.
(a) (0.1, 0.6) dominates (0.2, 0.5) because it has a lower FPR (0.1 < 0.2) AND a higher TPR (0.6 > 0.5).
(b) (0.1, 0.6) and (0.2, 0.7) are incomparable. The second has higher TPR (+0.1) but worse FPR (+0.1). Neither is strictly better.
(c)
(0.2, 0.7) is closer to the northwest ideal (0,1) by Euclidean distance, so it would be selected over (0.1, 0.6) using this specific metric.
On a larger, more realistic dataset (a few hundred samples rather than 10 toy instances), the same construction produces a smooth ROC curve. Here we fit a classifier, sweep every threshold with sklearn.metrics.roc_curve, and use the Euclidean-distance method from §2.11 to pick the operating threshold automatically.
The distance method picks the point closest to the ideal corner (0, 1) — here corresponding to a decision threshold of 0.263, not the default 0.5.
A binary classifier on 1,000 test samples produces: TP = 120, FN = 60, FP = 40, TN = 780.
(a) Confusion matrix:
| Predicted | Total | |||
|---|---|---|---|---|
| + | − | |||
| True | + | 120 (TP) | 60 (FN) | 180 |
| − | 40 (FP) | 780 (TN) | 820 | |
| Total | 160 | 840 | 1000 | |
(b) Accuracy = (120 + 780)/1000 = 0.900 (90%).
(c)
Classifier run on n = 500 samples, positive rate = 10% (50 sick / 450 healthy). Results: 40 sick correctly caught, 90 healthy incorrectly flagged.
(a) True positives: 40. Total real positives 50 → FN = 10. FP = 90 given. Total healthy = 450 → TN = 450 − 90 = 360. Matrix: TP 40 / FN 10 / FP 90 / TN 360.
(b)
(c)
F2 (≈ 0.606) is substantially higher than F1 (≈ 0.444) because β = 2 up-weights Recall, which this model does relatively well on (80%), while caring less about its poor Precision (30.8%). The "all caught but noisy" character of the model is rewarded as β grows.
Continuing the same 10 test instances from §3.3 (Construct ROC by Hand), build the full ROC table by sweeping the threshold through every unique P(+|x) value. As a reminder, the dataset is:
| Instance | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| P(+|x) | 0.95 | 0.93 | 0.87 | 0.85 | 0.80 | 0.78 | 0.76 | 0.53 | 0.43 | 0.25 |
| True Class | + | + | − | + | − | + | − | − | − | + |
Each unique P(+|x) score becomes a threshold. For each one, count how many rows are predicted positive (score ≥ threshold) and derive TP/FP/TN/FN, then TPR, FPR, Euc, and Youden's J.
| Threshold ≥ | Predict +: rows | TP | FP | TN | FN | TPR | FPR | Euc | J (Youden) |
|---|---|---|---|---|---|---|---|---|---|
| 1.00 (nobody) | ∅ | 0 | 0 | 5 | 5 | 0.0 | 0.0 | 1.000 | 0.0 |
| 0.95 | {1} | 1 | 0 | 5 | 4 | 0.2 | 0.0 | 0.800 | 0.2 |
| 0.93 | {1,2} | 2 | 0 | 5 | 3 | 0.4 | 0.0 | 0.600 | 0.4 |
| 0.87 | {1,2,3} | 2 | 1 | 4 | 3 | 0.4 | 0.2 | 0.632 | 0.2 |
| 0.85 | 1–4 | 3 | 1 | 4 | 2 | 0.6 | 0.2 | 0.447 | 0.4 |
| 0.80 | 1–5 | 3 | 2 | 3 | 2 | 0.6 | 0.4 | 0.566 | 0.2 |
| 0.78 | 1–6 | 4 | 2 | 3 | 1 | 0.8 | 0.4 | 0.447 | 0.4 |
| 0.76 | 1–7 | 4 | 3 | 2 | 1 | 0.8 | 0.6 | 0.632 | 0.2 |
| 0.53 | 1–8 | 4 | 4 | 1 | 1 | 0.8 | 0.8 | 0.825 | 0.0 |
| 0.43 | 1–9 | 4 | 5 | 0 | 1 | 0.8 | 1.0 | 1.020 | −0.2 |
| 0.25 (all) | 1–10 | 5 | 5 | 0 | 0 | 1.0 | 1.0 | 1.000 | 0.0 |
Step-function AUC for this n = 10 toy set ≈ 0.68 (trapezoidal rule). Compare to the smoother, larger-sample curve in the Python demo below (AUC = 0.880).
Minimum Euc = 0.447 occurs at two tied points: (0.85 threshold, FPR = 0.2, TPR = 0.6) and (0.78 threshold, FPR = 0.4, TPR = 0.8). A tie! The first has higher Precision (low FPR), the second has higher Recall (high TPR) — deployment cost structure picks between them.
Maximum Youden J = 0.4 is achieved by (0.93 threshold, 0.4/0), (0.85 threshold, 0.6/0.2), and (0.78 threshold, 0.8/0.4). A 3-way tie that reflects the small n = 10 test set.
Two candidate threshold points on ROC: Point M = (FPR 0.15, TPR 0.75), Point N = (FPR 0, TPR 0.66).
(a)
Euc prefers M.
(b)
J prefers N — the two criteria disagree.
(c) Euc weights "distance from the corner" in squared Euclidean space, so small movements near the TPR = 1 axis count more. Youden treats TPR and FPR linearly equal. Here, N buys a lower FPR (0.15 → 0) at the cost of some TPR (0.75 → 0.66); J calls this a good trade since it just adds the two changes, but Euc calls it a bad trade since squaring makes the TPR loss weigh more than the FPR gain. If missing a positive (low TPR) and a false alarm (high FPR) cost literally the same dollar amount, use J. If getting near-perfect TPR is disproportionately important (medical), Euc (or the equivalent β-heavy Fβ metric) is more natural.
Ad-tech task: Out of 10,000 ad impressions, only 100 users click (positive). Our model predicts 150 clicks total. Of its 150 predicted clicks, 60 are real (TP) and 90 are wrong (FP). Of the 100 real clicks it missed 40 (FN).
(a) TP = 60; FN = 40; FP = 90; TN = 10000 − 60 − 40 − 90 = 9810.
(b)
(c) F0.5 ≈ 0.429 < F1 ≈ 0.480 because β < 1 weights precision more heavily. This model has P = 40% (worse) and R = 60% (better) — downgrading the good metric and upgrading the bad one makes the harmonic mean drop, which correctly reflects the advertiser's pain of wasting budget on 90 non-clickers for every 60 real clicks.
A model reports AUC = 0.85 on a binary classification test set with 500 positives and 500 negatives. Suppose I take a uniformly random positive and a uniformly random negative and compare their P(+|X) scores. What's the probability the positive's score is strictly greater? If I compare 100 independent positive-negative pairs, how many do I expect to be correctly ordered?
Probability of correct ordering = AUC = 85% (that is exactly the probabilistic interpretation of AUC!). Expected number out of 100 independent pairs = 100 × 0.85 = 85 correctly ordered.
Four ROC operating points with (FPR, TPR) = (0.02, 0.60), (0.05, 0.80), (0.20, 0.96), (0.50, 0.99). Find: (a) Euclidean-minimizing, (b) Youden J-maximizing, (c) the choice for a costly-miss disease screening where TPR is 5× more important than FPR, and (d) the choice for a spam filter where FP (good→spam) costs 10× a FN (spam in inbox).
Eucs: (0.02, 0.60) → √(0.4² + 0.02²) = 0.4005; (0.05, 0.80) → √(0.2² + 0.05²) = 0.206; (0.20, 0.96) → √(0.04² + 0.20²) ≈ 0.204; (0.50, 0.99) → √(0.01 + 0.25) = 0.51. (a) Min Euc ≈ (0.20, 0.96) (by a hair over 0.80/0.05).
Js: 0.58, 0.75, 0.76, 0.49. (b) Max J = (0.20, 0.96).
(c) Disease screening: TPR dominates. Choose (0.20, 0.96) → 96% of cases caught, accepting 20% false alarm rate (which is manageable, it just means more tests). If you can go even higher TPR at any cost, pick (0.50, 0.99).
(d) Spam filter: FPR cost dominates. Pick the lowest achievable FPR point that still catches meaningful spam: (0.02, 0.60). Only 2% of ham goes to spam folder. You miss 40% of spam (that's the tradeoff) — add a second layer or accept it.
Answer all 6 questions. Click an option for instant feedback.
Your score: 0 / 6